The goals / steps of this project are the following:
import numpy as np
import cv2
import glob
import pickle
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline
# Prepare object points
objp = np.zeros((6*9,3), np.float32)
objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2)
# Arrays to store object points and image points from all the images.
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.
# Make a list of calibration images
images = glob.glob('./camera_cal/calibration*.jpg')
fig,axs = plt.subplots(10, 2, figsize=(40,100))
axs = axs.ravel()
# Step through the list and search for chessboard corners
for i, fname in enumerate(images):
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# Find the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (9,6),None)
# If found, add object points, image points
if ret == True:
objpoints.append(objp)
imgpoints.append(corners)
# Draw and display the corners
img = cv2.drawChessboardCorners(img, (9,6), corners, ret)
axs[i].imshow(img)
# Calculate camera matrix and distortion coefficients
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img.shape[1:], None, None)
# Save camera paramters
dist_pickle = {}
dist_pickle["mtx"] = mtx
dist_pickle["dist"] = dist
pickle.dump( dist_pickle, open( "calibration.p", "wb" ) )
# Read in an image
img = cv2.imread('./camera_cal/calibration1.jpg')
def cal_undistort(img, objpoints, imgpoints):
"""
Take an image, object points, and image points
Perform camera calibration and image distortion correction
Return the undistorted image
"""
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img.shape[1:], None, None)
undist = cv2.undistort(img, mtx, dist, None, mtx)
return undist
undistorted = cal_undistort(img, objpoints, imgpoints)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(24, 9))
f.tight_layout()
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=50)
ax2.imshow(undistorted)
ax2.set_title('Undistorted Image', fontsize=50)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
plt.savefig('./output_images/undistort_output.jpg')
def corners_unwarp(img, nx, ny, mtx, dist, draw_corners_flag):
"""
Take an image and do the following steps:
1) Undistort using mtx and dist
2) Convert to grayscale
3) Find the chessboard corners
4) If corners found:
a) draw corners
b) define 4 source points src = np.float32([[,],[,],[,],[,]])
Note: you could pick any four of the detected corners
as long as those four corners define a rectangle
c) define 4 destination points dst = np.float32([[,],[,],[,],[,]])
d) use cv2.getPerspectiveTransform() to get M, the transform matrix
e) use cv2.warpPerspective() to warp your image to a top-down view
Return the warped image and perspective transform matrix M
"""
# Remove distortion
undist = cv2.undistort(img, mtx, dist, None, mtx)
# Convert to grayscale
gray = cv2.cvtColor(undist, cv2.COLOR_BGR2GRAY)
# Search for corners
ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
M = []
warped = []
if ret == True:
# If we found corners, draw them
if draw_corners_flag:
cv2.drawChessboardCorners(undist, (nx, ny), corners, ret)
# Choose offset (for dst points) from image corners for plotting purposes
offset = 100
# Grab the image shape
img_size = (gray.shape[1], gray.shape[0])
# Grab the outer four detected corners for source points
src = np.float32([corners[0], corners[nx-1], corners[-1], corners[-nx]])
# Choose points for destination points (for displaying our warped result)
dst = np.float32([[offset, offset], [img_size[0]-offset, offset],
[img_size[0]-offset, img_size[1]-offset],
[offset, img_size[1]-offset]])
warped, M, Minv = unwarp(img, src, dst)
return warped, M
def unwarp(img, src, dst):
"""
Take an image and unwarp it.
Return the warped image, the transform matrix M, and the inverse Minv
"""
# Grab the image shape
img_size = (img.shape[1], img.shape[0])
# Given src and dst points, calculate the perspective transform matrix
M = cv2.getPerspectiveTransform(src, dst)
Minv = cv2.getPerspectiveTransform(dst, src)
# Warp the image using OpenCV warpPerspective()
warped = cv2.warpPerspective(img, M, img_size)
return warped, M, Minv
images = glob.glob('./camera_cal/calibration*.jpg')
nx = 9 # the number of inside corners in x
ny = 6 # the number of inside corners in y
# Step through the list and search for chessboard corners
for fname in images:
# Read an image
img = cv2.imread(fname)
# Warp the image
warped, perspective_M = corners_unwarp(img, nx, ny, mtx, dist, False)
if warped != []:
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(warped)
ax2.set_title('Undistorted and Warped Image', fontsize=30)
# Plot and save output images
def plot_save_two_images(img1, img2, title1, title2, filename):
# Plot the result
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
f.tight_layout()
ax1.imshow(img1)
ax1.set_title(title1, fontsize=30)
ax2.imshow(img2, cmap='gray')
ax2.set_title(title2, fontsize=30)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
plt.savefig(filename)
# Read the image
img = cv2.imread('./test_images/straight_lines1.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Undistort the image
undist = cv2.undistort(img, mtx, dist, None, mtx)
undist_copy = undist.copy()
# Pick four source points
left_bottom = (185, 720)
left_top = (585, 455)
right_bottom = (1125, 720)
right_top = (695, 455)
src = np.float32([left_top, right_top, right_bottom, left_bottom])
# Pick four destination points
offset = 200
height, width, _ = img.shape
dst = np.float32([(offset, 0), (width-offset, 0), (width-offset, height), (offset, height)])
# Unwarp the image
top_down, M, Minv = unwarp(undist, src, dst)
# Plot results
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(undist)
ax2.set_title('Undistorted Image', fontsize=30)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
plt.savefig('./output_images/straight_lines1_undistort_output.jpg')
cv2.line(top_down, (offset, 0), (offset, height), [255, 0, 0], 4)
cv2.line(top_down, (offset, 0), (width-offset, 0), [255, 0, 0], 4)
cv2.line(top_down, (width-offset, 0), (width-offset, height), [255, 0, 0], 4)
cv2.line(top_down, (width-offset, height), (offset, height), [255, 0, 0], 4)
fname = './test_images/straight_lines1.jpg'
fdir = './output_images/'
plot_save_two_images(img, top_down, 'Original Image', 'Perspective Transformed', fdir+'straight_lines1_perspective_transform_output.jpg')
The goal is to identify pixels where the gradient of an image falls within a specified threshold range.
def abs_sobel_thresh(img, orient='x', sobel_kernel=3, thresh=(0, 255)):
"""
Take an image, gradient orientation, and threshold min / max values.
Apply Sobel.
Return a binary image.
"""
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Apply x or y gradient with the OpenCV Sobel() function
# and take the absolute value
if orient == 'x':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel))
if orient == 'y':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel))
# Rescale back to 8 bit integer
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
# Create a copy and apply the threshold
binary_output = np.zeros_like(scaled_sobel)
# Apply thresholds
binary_output[(scaled_sobel >= thresh[0]) & (scaled_sobel <= thresh[1])] = 1
# Return the result
return binary_output
def mag_thresh(img, sobel_kernel=3, thresh=(0, 255)):
"""
Take an image, sobel kernel size, and threshold values
Apply thresholds on magnitude of Sobel gradients
Return the binary image.
"""
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Take both Sobel x and y gradients
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
# Calculate the gradient magnitude
gradmag = np.sqrt(sobelx**2 + sobely**2)
# Rescale to 8 bit
scale_factor = np.max(gradmag)/255
gradmag = (gradmag/scale_factor).astype(np.uint8)
# Create a binary image of ones where threshold is met, zeros otherwise
binary_output = np.zeros_like(gradmag)
binary_output[(gradmag >= thresh[0]) & (gradmag <= thresh[1])] = 1
# Return the binary image
return binary_output
def dir_threshold(img, sobel_kernel=3, thresh=(0, np.pi/2)):
"""
Take an image, sobel kernel size, and threshold values
Apply thresholds on direction of Sobel gradients
Return the binary image.
"""
# Grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Calculate the x and y gradients
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
# Take the absolute value of the gradient direction,
# apply a threshold, and create a binary image result
absgraddir = np.arctan2(np.absolute(sobely), np.absolute(sobelx))
binary_output = np.zeros_like(absgraddir)
binary_output[(absgraddir >= thresh[0]) & (absgraddir <= thresh[1])] = 1
# Return the binary image
return binary_output
# Read the test images
images = glob.glob('./test_images/*.jpg')
# Sobel kernel size
ksize = 3
# Step through the images and apply thresholding
for fname in images:
img = cv2.imread(fname)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Apply each of the thresholding functions
gradx = abs_sobel_thresh(img, orient='x', sobel_kernel=ksize, thresh=(10, 100))
grady = abs_sobel_thresh(img, orient='y', sobel_kernel=ksize, thresh=(10, 100))
mag_binary = mag_thresh(img, sobel_kernel=ksize, thresh=(50, 150))
dir_binary = dir_threshold(img, sobel_kernel=ksize, thresh=(.7, 1))
# Combine thresholds
combined = np.zeros_like(dir_binary)
combined[((gradx == 1) & (grady == 1)) | ((mag_binary == 1) & (dir_binary == 1))] = 1
# Plot the result
f, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(1, 6, figsize=(20, 10))
f.tight_layout()
ax1.imshow(img)
ax1.set_title('Original', fontsize=20)
ax2.imshow(gradx, cmap='gray')
ax2.set_title('X Gradient', fontsize=20)
ax3.imshow(grady, cmap='gray')
ax3.set_title('Y Gradient', fontsize=20)
ax4.imshow(mag_binary, cmap='gray')
ax4.set_title('Magnitude', fontsize=20)
ax5.imshow(dir_binary, cmap='gray')
ax5.set_title('Direction', fontsize=20)
ax6.imshow(combined, cmap='gray')
ax6.set_title('Combined', fontsize=20)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
def hls_select(img, thresh=(0, 255)):
"""
Take an image and threshold values
Threshold the S-channel of HLS
Return the binary image
"""
hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)
s_channel = hls[:,:,2]
binary_output = np.zeros_like(s_channel)
binary_output[(s_channel > thresh[0]) & (s_channel <= thresh[1])] = 1
return binary_output
def thresholding_pipeline_hyper_parameters(image, s_thresh=(0, 255),
sx_thresh=(0, 255),
sy_thresh=(0,255),
smag_thresh=(0, 255),
sdir_thresh= (0,1.5),
s_flag=False, sx_flag=False, sy_flag=False, smag_flag=False, sdir_flag=False):
"""
Take an image and threshold values
Combine thresholding of the S-channel of HLS and X gradient with Sobel
Return the combines binary image
"""
img = np.copy(image)
# Sobel X gradient
gradx_binary = abs_sobel_thresh(img, orient='x', thresh=sx_thresh)
# Sobel Y gradient
grady_binary = abs_sobel_thresh(img, orient='y', thresh=sy_thresh)
# Sobel gradient magnitude
mag_binary = mag_thresh(img, sobel_kernel=3, thresh=smag_thresh)
# Gradient direction
dir_binary = dir_threshold(img, sobel_kernel=15, thresh=sdir_thresh)
# S-channel thresholding
s_binary = hls_select(img, thresh=s_thresh)
# Combine the two binary thresholds
combined_binary = np.zeros_like(gradx_binary)
combined_binary[((s_binary == 1) & s_flag)
| ((gradx_binary == 1) & sx_flag)
| ((grady_binary == 1) & sy_flag)
| ((mag_binary == 1) & smag_flag)
| ((dir_binary == 1) & sdir_flag)] = 1
return combined_binary
fname = './test_images/straight_lines1.jpg'
fdir = './output_images/'
img = cv2.imread(fname)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Run the thresholding function
# Sobel X gradient
gradx_binary = thresholding_pipeline_hyper_parameters(img, sx_thresh=(10, 100), sx_flag=True)
plot_save_two_images(img, gradx_binary, 'Original Image', 'Gradient X', fdir+'straight_lines1_sobel_x.jpg')
# Sobel Y gradient
grady_binary = thresholding_pipeline_hyper_parameters(img, sy_thresh=(20, 100), sy_flag=True)
plot_save_two_images(img, grady_binary, 'Original Image', 'Gradient Y', fdir+'straight_lines1_sobel_y.jpg')
# Sobel gradient magnitude
mag_binary = thresholding_pipeline_hyper_parameters(img, smag_thresh=(30, 100), smag_flag=True)
plot_save_two_images(img, mag_binary, 'Original Image', 'Magnitude', fdir+'straight_lines1_sobel_magnitude.jpg')
# Gradient direction
dir_binary = thresholding_pipeline_hyper_parameters(img, sdir_thresh=(0.8, 1.2), sdir_flag=True)
plot_save_two_images(img, dir_binary, 'Original Image', 'Direction', fdir+'straight_lines1_sobel_direction.jpg')
# S-channel thresholding
hls_binary = thresholding_pipeline_hyper_parameters(img, s_thresh=(90, 255), s_flag=True)
plot_save_two_images(img, hls_binary, 'Original Image', 'S Channel', fdir+'straight_lines1_hls.jpg')
# Combined result
combined_binary = thresholding_pipeline_hyper_parameters(img,
s_thresh=(170, 255),
sx_thresh=(20, 100),
sy_thresh=(20,100),
smag_thresh=(20, 100),
sdir_thresh= (0.8,1.2),
s_flag=True, sx_flag=True, sy_flag=False, smag_flag=False, sdir_flag=False)
plot_save_two_images(img, combined_binary, 'Original Image', 'Combined', fdir+'straight_lines1_thresholding_output.jpg')
def thresholding_pipeline(image, s_thresh=(170, 255), sx_thresh=(20, 100)):
"""
Take an image and threshold values
Combine thresholding of the S-channel of HLS and X gradient with Sobel
Return the combines binary image
"""
img = np.copy(image)
# Convert to HLS color space and separate the V channel
hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)
l_channel = hls[:,:,1]
s_channel = hls[:,:,2]
# Sobel x
sobelx = cv2.Sobel(l_channel, cv2.CV_64F, 1, 0)
abs_sobelx = np.absolute(sobelx)
scaled_sobel = np.uint8(255*abs_sobelx/np.max(abs_sobelx))
# Threshold x gradient
sxbinary = np.zeros_like(scaled_sobel)
sxbinary[(scaled_sobel >= sx_thresh[0]) & (scaled_sobel <= sx_thresh[1])] = 1
# Threshold color channel
s_binary = np.zeros_like(s_channel)
s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 1
# Combine the two binary thresholds
combined_binary = np.zeros_like(sxbinary)
combined_binary[(s_binary == 1) | (sxbinary == 1)] = 1
return combined_binary
# Test thresholding pipline for all test images
images = glob.glob('./test_images/*.jpg')
test_images = []
# Step through the images
for fname in images:
# Read the image
img = cv2.imread(fname)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Undistort the image
undist = cv2.undistort(img, mtx, dist, None, mtx)
undist_copy = undist.copy()
# Threshold
binary_image = thresholding_pipeline(undist)
# Pick four source points
left_bottom = (185, 720)
left_top = (585, 455)
right_bottom = (1125, 720)
right_top = (695, 455)
src = np.float32([left_top, right_top, right_bottom, left_bottom])
# Pick four destination points
offset = 200
height, width = binary_image.shape
dst = np.float32([(offset, 0), (width-offset, 0), (width-offset, height), (offset, height)])
# Unwarp the image
top_down, M, Minv = unwarp(binary_image, src, dst)
# Plot results
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(40, 20))
ax1.imshow(undist_copy)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(binary_image)
ax2.set_title('Thresholded', fontsize=30)
ax3.imshow(top_down)
ax3.set_title('Top Down', fontsize=30)
test_images.append({'file':fname, 'image':img, 'threshold':binary_image, 'top_down': top_down})
# Saving sample results to show in README.md
fname = './test_images/straight_lines1.jpg'
fdir = './output_images/'
plot_save_two_images(test_images[7]['image'], test_images[7]['top_down'], 'Original Image', 'Thresholding + Perspective Transform', fdir+'straight_lines1_top_down_output.jpg')
def hist(img):
# Grab only the bottom half of the image. Lane lines are likely to be mostly vertical nearest to the car.
bottom_half = img[img.shape[0]//2:,:]
# Sum across image pixels vertically
histogram = np.sum(bottom_half, axis=0)
return histogram
# Step through the images
for image_index in test_images:
# Normalize the image
img = image_index['top_down']/255
# Create histogram of image binary activations
histogram = hist(img)
# Visualize the resulting histogram
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(40, 20))
ax1.imshow(image_index['top_down'])
ax1.set_title('Top Down', fontsize=30)
ax2.plot(histogram)
ax2.set_title('Histogram', fontsize=30)
def find_lane_pixels(binary_warped):
# Take a histogram of the bottom half of the image
histogram = np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis=0)
# Create an output image to draw on and visualize the result
out_img = np.dstack((binary_warped, binary_warped, binary_warped))
# Find the peak of the left and right halves of the histogram
# These will be the starting point for the left and right lines
midpoint = np.int(histogram.shape[0]//2)
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
# HYPERPARAMETERS
# Choose the number of sliding windows
nwindows = 9
# Set the width of the windows +/- margin
margin = 150
# Set minimum number of pixels found to recenter window
minpix = 50
# Set height of windows - based on nwindows above and image shape
window_height = np.int(binary_warped.shape[0]//nwindows)
# Identify the x and y positions of all nonzero pixels in the image
nonzero = binary_warped.nonzero()
nonzeroy = np.array(nonzero[0])
nonzerox = np.array(nonzero[1])
# Current positions to be updated later for each window in nwindows
leftx_current = leftx_base
rightx_current = rightx_base
# Create empty lists to receive left and right lane pixel indices
left_lane_inds = []
right_lane_inds = []
# Step through the windows one by one
for window in range(nwindows):
# Identify window boundaries in x and y (and right and left)
win_y_low = binary_warped.shape[0] - (window+1)*window_height
win_y_high = binary_warped.shape[0] - window*window_height
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
win_xright_low = rightx_current - margin
win_xright_high = rightx_current + margin
# Draw the windows on the visualization image
cv2.rectangle(out_img,(win_xleft_low,win_y_low),
(win_xleft_high,win_y_high),(0,255,0), 2)
cv2.rectangle(out_img,(win_xright_low,win_y_low),
(win_xright_high,win_y_high),(0,255,0), 2)
# Identify the nonzero pixels in x and y within the window #
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
# Append these indices to the lists
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
# If you found > minpix pixels, recenter next window on their mean position
if len(good_left_inds) > minpix:
leftx_current = np.int(np.mean(nonzerox[good_left_inds]))
if len(good_right_inds) > minpix:
rightx_current = np.int(np.mean(nonzerox[good_right_inds]))
# Concatenate the arrays of indices (previously was a list of lists of pixels)
try:
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)
except ValueError:
# Avoids an error if the above is not implemented fully
pass
# Extract left and right line pixel positions
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
return leftx, lefty, rightx, righty, out_img
def fit_polynomial(binary_warped):
"""
Find our lane pixels first
"""
leftx, lefty, rightx, righty, out_img = find_lane_pixels(binary_warped)
# Fit a second order polynomial to each using `np.polyfit`
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
# Generate x and y values for plotting
ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0] )
try:
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
except TypeError:
# Avoids an error if `left` and `right_fit` are still none or incorrect
print('The function failed to fit a line!')
left_fitx = 1*ploty**2 + 1*ploty
right_fitx = 1*ploty**2 + 1*ploty
## Visualization ##
# Colors in the left and right lane regions
out_img[lefty, leftx] = [255, 0, 0]
out_img[righty, rightx] = [0, 0, 255]
return out_img, left_fit, right_fit, ploty
fname = './test_images/straight_lines1.jpg'
fdir = './output_images/'
# Read the image
binary_warped = test_images[1]['top_down']
out_img, left_fit, right_fit, ploty = fit_polynomial(binary_warped)
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
# Plot the result
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
f.tight_layout()
ax1.imshow(binary_warped)
ax1.set_title('Binary Warped', fontsize=30)
#ax2.imshow(out_img, cmap='gray')
ax2.set_title('Polynomial Fit', fontsize=30)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
ax2.plot(left_fitx, ploty, color='yellow', linewidth=10)
ax2.plot(right_fitx, ploty, color='yellow', linewidth=10)
ax2.imshow(out_img)
plt.savefig(fdir+'straight_lines1_polynomial_output.jpg')
def fit_poly(img_shape, leftx, lefty, rightx, righty):
"""
Fit a second order polynomial to each lane
"""
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
# Generate x and y values for plotting
ploty = np.linspace(0, img_shape[0]-1, img_shape[0])
# Calc both polynomials
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
return left_fit, right_fit, left_fitx, right_fitx, ploty
def search_around_poly(binary_warped, left_fit, right_fit):
# HYPERPARAMETER
# Choose the width of the margin around the previous polynomial to search
margin = 200
# Grab activated pixels
nonzero = binary_warped.nonzero()
nonzeroy = np.array(nonzero[0])
nonzerox = np.array(nonzero[1])
# Set the area of search based on activated x-values
left_lane_inds = ((nonzerox > (left_fit[0]*(nonzeroy**2) + left_fit[1]*nonzeroy +
left_fit[2] - margin)) & (nonzerox < (left_fit[0]*(nonzeroy**2) +
left_fit[1]*nonzeroy + left_fit[2] + margin)))
right_lane_inds = ((nonzerox > (right_fit[0]*(nonzeroy**2) + right_fit[1]*nonzeroy +
right_fit[2] - margin)) & (nonzerox < (right_fit[0]*(nonzeroy**2) +
right_fit[1]*nonzeroy + right_fit[2] + margin)))
# Extract left and right line pixel positions
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
# Fit new polynomials
left_fit, right_fit, left_fitx, right_fitx, ploty = fit_poly(binary_warped.shape, leftx, lefty, rightx, righty)
## Visualization ##
# Create an image to draw on and an image to show the selection window
out_img = np.dstack((binary_warped, binary_warped, binary_warped))*255
window_img = np.zeros_like(out_img)
# Color in left and right line pixels
out_img[nonzeroy[left_lane_inds], nonzerox[left_lane_inds]] = [255, 0, 0]
out_img[nonzeroy[right_lane_inds], nonzerox[right_lane_inds]] = [0, 0, 255]
# Generate a polygon to illustrate the search window area
left_line_window1 = np.array([np.transpose(np.vstack([left_fitx-margin, ploty]))])
left_line_window2 = np.array([np.flipud(np.transpose(np.vstack([left_fitx+margin,
ploty])))])
left_line_pts = np.hstack((left_line_window1, left_line_window2))
right_line_window1 = np.array([np.transpose(np.vstack([right_fitx-margin, ploty]))])
right_line_window2 = np.array([np.flipud(np.transpose(np.vstack([right_fitx+margin,
ploty])))])
right_line_pts = np.hstack((right_line_window1, right_line_window2))
# Draw the lane onto the warped blank image
cv2.fillPoly(window_img, np.int_([left_line_pts]), (0,255, 0))
cv2.fillPoly(window_img, np.int_([right_line_pts]), (0,255, 0))
result = cv2.addWeighted(out_img, 1, window_img, 0.3, 0)
return result, left_fit, right_fit, ploty
test_images[3]['top_down']
# Test on a sample images
binary_warped = test_images[4]['top_down']
# Polynomial fit values from the previous frame
left_fit = np.array([ 2.13935315e-04, -3.77507980e-01, 4.76902175e+02])
right_fit = np.array([4.17622148e-04, -4.93848953e-01, 1.11806170e+03])
ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0])
left_fitx_prev = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx_prev = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
# Run image through the pipeline
# Note that in your project, you'll also want to feed in the previous fits
result, left_fit, right_fit, ploty = search_around_poly(binary_warped, left_fit, right_fit)
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
# Visualize the resulting histogram
fig, ax = plt.subplots(figsize=(20, 10))
plt.plot(left_fitx_prev, ploty, color='red')
plt.plot(right_fitx_prev, ploty, color='red')
plt.plot(left_fitx, ploty, color='yellow')
plt.plot(right_fitx, ploty, color='yellow')
ax.imshow(result)
def measure_curvature_real(left_fit_cr, right_fit_cr, y_eval):
'''
Calculates the curvature of polynomial functions in meters.
'''
# Define conversions in x and y from pixels space to meters
ym_per_pix = 30/720 # meters per pixel in y dimension
xm_per_pix = 3.7/700 # meters per pixel in x dimension
# Calculation of R_curve (radius of curvature)
if left_fit_cr[0] != 0:
left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0])
else:
left_curverad = 1000000
if right_fit_cr[0] !=0:
right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0])
else:
right_curverad = 1000000
return left_curverad, right_curverad
def measure_distance_to_center_real(left_fit_cr, right_fit_cr, image_shape):
'''
Calculates the distance to center of the lane in meters.
'''
# Define conversions in x and y from pixels space to meters
ym_per_pix = 30/720 # meters per pixel in y dimension
xm_per_pix = 3.7/700 # meters per pixel in x dimension
y_eval = image_shape[0]
left_x_eval = left_fit_cr[0]*y_eval**2 + left_fit_cr[1]*y_eval + left_fit_cr[2]
right_x_eval = right_fit_cr[0]*y_eval**2 + right_fit_cr[1]*y_eval + right_fit_cr[2]
lane_center_position = (right_x_eval + left_x_eval) /2
image_center_position = image_shape[1] / 2
return (lane_center_position - image_center_position) * xm_per_pix
def measure_line_distance_to_center_real(line_fit_cr, m_per_pix, image_shape):
'''
Calculates the distance from line to center of the image in meters.
'''
y_eval = image_shape[0]
line_x_eval = line_fit_cr[0]*y_eval**2 + line_fit_cr[1]*y_eval + line_fit_cr[2]
image_center_position = image_shape[1] / 2
return (line_x_eval - image_center_position) * m_per_pix
def draw_lanes_on_undistorted(image, top_down, left_fit, right_fit, ploty):
"""
Draw lanes on the image
"""
zeros = np.zeros_like(top_down).astype(np.uint8)
lane_mask = np.dstack((zeros, zeros, zeros))
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
l_line_pts = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
r_line_pts = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))])
pts = np.hstack((l_line_pts, r_line_pts))
cv2.fillPoly(lane_mask, np.int_([pts]), (50,255,50))
cv2.polylines(lane_mask, np.int32([l_line_pts]), isClosed=False, color=(50,50,255), thickness=20)
cv2.polylines(lane_mask, np.int32([r_line_pts]), isClosed=False, color=(255,76,76), thickness=20)
img_size = (image.shape[1], image.shape[0])
lane_mask_unwarped = cv2.warpPerspective(lane_mask, Minv, img_size)
result = cv2.addWeighted(image, 1, lane_mask_unwarped, 0.5, 0)
return result
# Define a class to receive the characteristics of each line detection
class Line():
def __init__(self):
# was the line detected in the last iteration?
self.detected = False
# polynomial coefficients averaged and low-pass filtered over frames
self.best_fit = None
# polynomial coefficients for the most recent fit
self.current_fit = None
# radius of curvature of the line in meters unit
self.radius_of_curvature = None
# distance in meters of vehicle center from the line
self.line_base_pos = None
# number of consecutive bad frames
self.reset_counter = 0
def reset(self):
self.detected = False
self.current_fit = None
self.radius_of_curvature = None
self.line_base_pos = None
self.reset_counter = 0
class Boundaries():
def __init__(self, left_line, right_line):
self.right = left_line
self.left = right_line
curve_radius = None
distance_to_center = None
self.ploty = None
def update(self, top_down_image):
# Fit right and left line polynomials
if self.left.detected or self.right.detected:
result, left_fit_new, right_fit_new, ploty = search_around_poly(top_down_image, self.left.best_fit, self.right.best_fit)
self.ploty = ploty
else:
result, left_fit_new, right_fit_new, ploty = fit_polynomial(top_down_image)
self.ploty = ploty
if len(left_fit_new) == 0:
self.left.detected = False
self.left.current_fit = self.left.best_fit
left_fit_new = self.left.best_fit
# restart after 10 consecutive bad frames
if self.left.reset_counter == 10:
self.left.reset()
if len(right_fit_new) == 0:
self.right.detected = False
self.right.current_fit = self.right.best_fit
right_fit_new = self.right.best_fit
# restart after 10 consecutive bad frames
if self.right.reset_counter == 10:
self.right.reset()
# The curvature of the lane
left_curve, right_curve = measure_curvature_real(left_fit_new, right_fit_new, top_down_image.shape[0])
# Vehicle position with respect to center
left_base_pos = measure_line_distance_to_center_real(left_fit_new, 3.7/700, top_down_image.shape)
right_base_pos = measure_line_distance_to_center_real(right_fit_new, 3.7/700, top_down_image.shape)
distance_to_center = measure_distance_to_center_real(left_fit_new, right_fit_new, top_down.shape)
curve_radius = (left_curve + right_curve)/2
# Update best fits if lines pass our criteria
if (self.check_curvature(left_curve, right_curve, 3000) and self.check_separation(left_base_pos, right_base_pos, 3) and self.check_slope(left_fit_new, right_fit_new, top_down_image.shape[0], 1)) or ((self.left.best_fit is None) or (self.right.best_fit is None)):
self.left.current_fit = left_fit_new
self.right.current_fit = right_fit_new
if self.left.best_fit is not None:
self.left.best_fit = list(np.round(np.array(self.left.current_fit) * 0.1 + np.array(self.left.best_fit) * 0.9 , decimals=4))
else:
self.left.best_fit = self.left.current_fit
if self.right.best_fit is not None:
self.right.best_fit = list(np.round(np.array(self.right.current_fit) * 0.1 + np.array(self.right.best_fit) * 0.9 , decimals=4))
else:
self.right.best_fit = self.right.current_fit
# Update curvature
self.left.radius_of_curvature = left_curve
self.right.radius_of_curvature = right_curve
# Update vehicle position
self.left.line_base_pos = left_base_pos
self.right.line_base_pos = right_base_pos
self.distance_to_center = distance_to_center
self.curve_radius = curve_radius
self.left.detected = True
self.right.detected = True
self.reset_counter = 0
else:
self.left.detected = False
self.right.detected = False
self.left.reset_counter += 1
# restart after 10 consecutive bad frames
if self.left.reset_counter == 10:
self.left.reset()
self.right.reset_counter += 1
# restart after 10 consecutive bad frames
if self.right.reset_counter == 10:
self.right.reset()
def check_curvature(self, curve1, curve2, threshold):
similar_curvatures = False
if np.absolute(curve1 - curve2) < threshold:
similar_curvatures = True
return similar_curvatures
def check_separation(self, dist1, dist2, threshold):
separated_by_distance = False
if np.absolute(dist1 - dist2) > threshold:
separated_by_distance = True
return separated_by_distance
def check_slope(self, left_line, right_line, y_eval, threshold):
left_derivative = 2 * left_line[0]*y_eval + left_line[1]
right_derivative = 2 * right_line[0]*y_eval + right_line[1]
similar_slopes = False
if np.absolute(left_derivative - right_derivative) < threshold:
similar_slopes = True
return similar_slopes
def process_image(image):
"""
Complete process of one BGR image
"""
#global left_fit, right_fit, ploty
global left_fit, right_fit, boundaries
img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Undistort the image
undist = cv2.undistort(img, mtx, dist, None, mtx)
undist_copy = undist.copy()
# Threshold
binary_image = thresholding_pipeline(undist)
# Unwarp the image
left_bottom = (185, 720)
left_top = (585, 455)
right_bottom = (1125, 720)
right_top = (695, 455)
src = np.float32([left_top, right_top, right_bottom, left_bottom])
offset = 200
height, width = binary_image.shape
dst = np.float32([(offset, 0), (width-offset, 0), (width-offset, height), (offset, height)])
top_down, M, Minv = unwarp(binary_image, src, dst)
boundaries.update(top_down)
# Warp the detected lane boundaries back onto the original image
image_with_lane = draw_lanes_on_undistorted(image, top_down, left_fit.best_fit, right_fit.best_fit, boundaries.ploty)
# Add curvature of the lane and distance from center on top of the image
cv2.putText(image_with_lane, "Radius of Curvature: %s(m)" % int(boundaries.curve_radius), (100, 50), cv2.FONT_HERSHEY_DUPLEX, 1.5, [255, 255, 255], 2)
if boundaries.distance_to_center > 0:
cv2.putText(image_with_lane, "Vehicle is %.2fm left of center" % boundaries.distance_to_center, (100, 100), cv2.FONT_HERSHEY_DUPLEX, 1.5, [255, 255, 255], 2)
else:
cv2.putText(image_with_lane, "Vehicle is %.2fm right of center" % np.abs(boundaries.distance_to_center), (100, 100), cv2.FONT_HERSHEY_DUPLEX, 1.5, [255, 255, 255], 2)
# The final image
final_image = np.zeros((1080, 1280, 3), dtype=np.uint8)
final_image[360:1080, 0:1280] = image_with_lane
temp = np.dstack((binary_image, binary_image, binary_image)) * 255
final_image[0:360, 0:640] = cv2.resize(temp, (640,360), interpolation=cv2.INTER_AREA)
temp = np.dstack((top_down, top_down, top_down)) * 255
final_image[0:360, 640:1280] = cv2.resize(temp, (640,360), interpolation=cv2.INTER_AREA)
return final_image
left_fit = Line()
right_fit = Line()
boundaries = Boundaries(left_fit, right_fit)
fname = './test_images/test4.jpg'
fdir = './output_images/'
# Read the image
img = cv2.imread(fname)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Run image through the pipeline
processed_image = process_image(img)
# Plot the result
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
f.tight_layout()
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=30)
ax2.set_title('1) binary 2) top View 3) Lanes Overlaid', fontsize=30)
ax2.imshow(processed_image)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
plt.savefig(fdir+'test4_pipeline_output.jpg')
# Import everything needed to edit/save/watch video clips
from moviepy.editor import VideoFileClip
from IPython.display import HTML
left_fit = Line()
right_fit = Line()
boundaries = Boundaries(left_fit, right_fit)
video_output = 'output_project_video.mp4'
clip = VideoFileClip('project_video.mp4')
video_clip = clip.fl_image(process_image)
%time video_clip.write_videofile(video_output, audio=False)
HTML("""
<video width="960" height="540" controls>
<source src="{0}">
</video>
""".format(video_output))
left_fit = Line()
right_fit = Line()
boundaries = Boundaries(left_fit, right_fit)
video_output = 'output_challenge_video.mp4'
clip = VideoFileClip('challenge_video.mp4')
video_clip = clip.fl_image(process_image)
%time video_clip.write_videofile(video_output, audio=False)
HTML("""
<video width="960" height="540" controls>
<source src="{0}">
</video>
""".format(video_output))
left_fit = Line()
right_fit = Line()
boundaries = Boundaries(left_fit, right_fit)
video_output = 'output_harder_challenge_video.mp4'
clip = VideoFileClip('harder_challenge_video.mp4')
video_clip = clip.fl_image(process_image)
%time video_clip.write_videofile(video_output, audio=False)
HTML("""
<video width="960" height="540" controls>
<source src="{0}">
</video>
""".format(video_output))